某交付型 ERP 公司的 Lowcode 演进史

thumbnail

过去的十个月,我以项目技术负责人的身份,成功将整个 Lowcode 项目推向了 Release。期间遇到了很多的技术选择问题,同样也遇到了很多项目推进中会发生的,与技术无关的问题。这里做一个简单的总结,希望遗留下来的经验对后来者有所帮助。

交付公司为什么会做 Lowcode?

作为一家交付型 ERP 公司,为什么你们会希望做一款 Lowcode 产品呢?

说起公司做 Lowcode 的原因,其实上也挺简单的。一个核心思想:节约开销

可能很多人不理解,我司作为一家 ERP 厂商的实施方,为什么会有节约开销这种需求?这一点算是历史遗留问题,简单来说可以被总结为两个点:

  1. 交付团队人员变化快,接手人面对各式各样的技术会陷入迷茫,从而延长交付时间
  2. 客户需求变化快,交付团队有时会被拖入需求频繁变动的拉锯战中

我司作为一家以交付项目为主体的公司,客户需求自然是很难被直接管控的点,因此能够入手的地方也就只有 1 了,从人员结构和技术框架开始。

切入点 / 面向人群

让我们先来看看一个标准交付团队的人员组成:产品 + 后端 + 前端

再来看看公司内目前标准交付项目的技术栈组成:Mobx / Redux + React + Java(后端技术栈 etc)+ Dva(中台交付) / React-Router(研发交付)

可以看到,上述的技术栈组成中,最为繁杂的就是前端的各种库,这部分如非高级前端的话,很难短期内掌握。其次,交付团队中需要前端也是近几年前端技术栈复杂后的结果。

自然而然的,我们会提出一个问题:交付团队中真的需要高级前端的支撑吗

交付团队中的高级前端,能否被其他两类角色代替?

从这个角度出发去思考问题,我们会很惊讶的发现,目前公司交付项目上的业务普遍由单一的 CRUD 组成。而高级前端承接的任务,更多的是对前端项目进行配置,或者是使用后端不太了解的技术栈(Redux / React-Router)去搭建页面。这样说的话,我们需要关注的,是对这些前端特有的技术栈进行相关的封装,让后端也能完成前端开发所能完成的事情。

因此得出结论,我们的切入点在于取代高级前端,而面向的人群,是交付项目上另外两类角色。公司最终的愿景,在于将整个交付团队的人员削减到只有业务 / 产品的地步。

第一阶段

搞清楚面向人群后,第一阶段的产物就是 DataSet。这是一套基于 MobxAxios,封装了业务中常用 CRUD 逻辑的一套抽象产物。与典型的业务组件相搭配(如 Form / Table),从而让后端也能顺利的书写前端代码。它的前身是 JSP 时代,我司内部自行抽象的 Aurora

DataSet 这套产物,目前已经在全面服务我司内部的各种交付项目,并实现了第一个愿景,让后端代替前端的工作。它的出现让传统 CRUD 逻辑有了管控的地方,以及大大简化了前端代码书写的复杂程度。后端不必再和 Redux 这种前端组件库打交道,可以直接将自己很熟悉的数据库表结构套用到 DataSet 这套体系中。

同样的,DataSet 也是我即将提到的第二阶段,Lowcode 平台中模型的重要组成部分。

第二阶段

第一阶段中,我们让后端代替了前端,那么是否存在更进一步的方案,让项目中的产品 / 业务去替代后端的职责,从而大幅减少项目人员成本呢?

我司目前在交付项目上最常见的场景,便是头行表单。简而言之就是 Form + TableTable 选中的行有变动的时候,上面的 Form 也会随着变动,而且在 Form 中修改后的数据也会被同步到下面的 Table 中。

上述的这种场景算是简单场景之一,那么我们是否具备复用这种简单场景的能力呢?

如果我们有一个平台,可以用可视化的手段实现这些逻辑,并且可以通过某种手段,在下一次的搭建过程中复用上一次的搭建产物,这样不就可以回答上面这个问题了吗?

聪明的人一定会猜到,这个平台本身就是 LowcodeLowcode 作为可视化的搭建平台的重要作用就是收束之前项目的搭建产物,并使之能够在下一次的搭建流程中被复用,而可视化拖拽,则是为了降低交互门槛所采用的一种交互手段。

搭建这个平台的过程中势必会碰到各种各样的问题,经过 10 个月的踩坑,我将典型的问题归结为以下几种:

动态表单

Lowcode 刚起步的时候,左侧选中不同的组件,如何高效且有效的切换组件对应的属性面板?同时不让开发者进行反复的开发工作(比如每新建一个组件,就要书写一个对应的业务表单)?

这里需要感谢一下 gaea-editor,观察了他们的属性面板设计后,我发现属性面板实际上就是开发中存在很久的一个问题:可配置化的动态表单。要解决这一点,需要的是一个基于 IOC 设计模式的架构(类似 vscode),将每个组件中使用的公共组件抽离成插件,每个组件维护一套 config 数组,当选中的组件被切换的时候,切换到对应的 config,并依照这套规则去加载对应的插件,从而组合成一套全新的表单。

不过这里还是存在另外一个问题:

如果我们希望在未来加入 npm 组件导入功能,开发者书写的组件是多种多样的,如何根据他们书写的组件识别出对应的 config 呢?

熟悉 Lowcode 的同学应该就看出来了,这套设计在 云凤蝶 中出现过。起初我们最直观的想法就是:开发者自己去书写。这套思路虽然可行,但是会增加开发者接入 Lowcode 平台的复杂度,降低他们的接入意愿,因此这套流程应当是尽可能自动化的才对。

这个简单的问题整整困扰了我近半年时间,期间我出过各种各样的设计思路,但是都不尽如人意。直到年中的时候,同组的后端向我们抱怨前端组件的 json 在后端难以维护,希望给一套方案去校验的时候,上述这个问题才迎刃而解。

目前对于组件经常变动的属性数据,后端是直接用 json 字符串的形式去存储的,每次升级组件的时候,json 数据的升级都会是一个大问题。因为组件的属性是经常增删的,如果某些关键属性在后端的升级脚本中没有注意到的话,整个平台都会存在着崩溃的风险。这对于整个平台的维护并非是一个好消息。

起初我的想法也很简单,后端针对 json 所有的数据落一张表,然后 json 数据存到后端的时候,直接对照这张表去做校验即可。但是仔细深入思考一下,会出现一个很奇怪的问题:json 发展历程如此之久,真的没有人遇到跟我们一样的校验问题吗?

带着这个问题,我去查阅了社区对应的实现,最后找到了这个 keyPoint:json-schema

json-schema 用于描述并校验 json 格式,它类似一套结构化的规范,前端和后端均可使用 json-schema 去做 json 格式的校验。如果我们能够对 json 书写一套完备的 json-schema 的话,前后端自然就能省去数据校验不准确的各类问题了。

那么它和动态表单有什么联系呢?我们可以仔细看看 json-schema 的结构,并与 gaea-editorconfig 做一下对比

json-schema

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
     "name":  { "type": "string" },
     "email": { "type": "string" },
     "age": {
       "type": "integer",
       "minimum": 0,
       "exclusiveMinimum": false,
     },
     "telephone": {
       "type": "string",
       "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$"
     }
  },
  "required": ["name", "email"],
  "additionalProperties": false
}

gaea-editor

const editSetting = {
  key: 'gaea-button',
  name: 'Button',
  editors: [
    'Layout',
    {
      type: 'box-editor'
    },
    'Function',
    {
      field: 'text',
      text: 'Text',
      type: 'string'
    },
    {
      field: 'href',
      text: 'Href',
      type: 'string'
    },
    'Style',
    {
      field: 'ghost',
      text: 'Transparent',
      type: 'boolean'
    },
    {
      field: 'size',
      text: 'Size',
      type: 'select',
      data: [{
          value: null,
          text: 'Default'
        },
        {
          value: 'small',
          text: 'Small'
        },
        {
          value: 'large',
          text: 'Large'
        }
      ]
    }
  ],
  events: [{
    text: 'OnClick',
    field: 'onClick'
  }]
};

可以发现,两者之间还是存在不少共性的,那么紧接着会迎来另一个问题,json-schema 如果能直接生成表单的话,那我们岂不就不用单独维护一套属性面板的 config 吗?

这部分在业内也有很多的常用实现,我主要研究的是 form-renderer,这是利用 json-schema 与自己的特有属性结合实现的一套专有引擎。同样基于 IOC,只不过 config 换成了 json-schema 而已。

再仔细看看 json-schema,是否觉得这套东西跟某种东西有所共通呢?没错,就是 typescript 中的 interfaceinterfacejson-schema 做的事情也非常类似,都是针对某中东西做类型规约和校验。

如果我们把组件库中针对组件 Props 的 interface 提取出来转换成 json-schema,是不是就可以在不写一行代码的情况下,自动读取并生成属性面板呢?

借助 typescript-json-schema,我们可以将 typescript 组件库中的 interface 转换成 json-schema,然后利用 form-renderer 这类库翻译成对应的属性面板,这样就解决了上面的问题。

布局

布局这一点,不同的 Lowcode 业务会有不同的实现方式。如 云凤蝶 采用了自由画布,并且在这个特性上投入了大量资源。而 Mendix 这一类产品,就将研发的重点投入到了栅格布局上。因此实现如何,主要看的还是业务平台本身面向的人群。

因为我们能够投入的研发力量有限,不能像云凤蝶一样在画布方面大量投入技术资源,因此我们选择了更好实现的栅格布局作为我们的实现手段。

栅格布局的实现,整体来说也不算很难,需要关注的就是一个点即可:组件渲染逻辑。

我们在平台中规定了基础组件、数据组件以及容器组件这三种类型,然后将整个组件渲染逻辑做一次完整的规约,其中数据组件可以被理解为与表绑定的组件,基础组件则对应的是数据库表中的字段(所以基础组件不能独立于数据组件而单独存在),而容器组件,顾名思义就是用来承载上述两种组件的组件。同样的,容器组件也可以递归式的承载(即容器组件可以相互嵌套)。

确立了上述的渲染逻辑后,整个栅格布局就如同水到渠成,剩下的就是投入时间和精力去一一实现其中的各项功能了。

另外还有一个点会是组件树,组件树是为了解决组件相互嵌套后,用户如何能够通过便捷的操作去选中某一层级的组件这一需求的。有了渲染逻辑,只需要根据逻辑写一个简单的 dfs 就可以做到这一切。

当然,这并不是代表着我对自由画布不感兴趣。有机会的话,我还是希望能够探索一下自由画布的详细实现细节的。

逻辑编排

市面上大部分的 Lowcode 平台都有一个奇怪的问题,不重视逻辑编排功能,实际上我对这部分的实现一直非常感兴趣。一个成熟的 Lowcode 产品,必然会承载一些 CRUD 逻辑以外的东西,如何承载并维护好这些沉淀下来的业务逻辑,对我们来说是一个新的挑战。

针对产品面向的人群,以及我们目前能够投入的技术力量综合考评来看,我们采用了工作流式的实现方案,并且实现了一套前后端节点可以交替执行的特殊执行逻辑(由于涉密,这里不做详细展开)。

但是这不代表着工作流式的实现方案就是唯一的解,至少我见过的实现方式就有两套,这里我简单列一下:

  1. flume
  2. Node接入层可视化逻辑编排,还可以这样做?

这是两套不太一样的逻辑编排思路,第二种非常类似伪代码的实现,这套思路很适合面向人群非开发的群体,但是对于我们这种面向人群中存在产品的就不太适合。

因此,这一块的交互选型,依然是要根据产品使用情况,具体情况具体分析。

其实上还是有一些很有意思的点可以深挖的:比如 for 循环在面向开发和面向非开发时应该选择什么表现形式?以及逻辑编排中是否应该引入变量这一概念?甚至说是否应该在引擎执行时对 AST 树进行分析并优化?如何针对逻辑 debug 引入一套完善的体系?这些点都是值得深入发掘并深究的,以后有机会的话,会再输出一篇文章讲这件事情。

产品的未来规划

目前我为整个 Lowcode 项目组列下的计划中,主要包含以下几个方向:

  1. 移动端 Lowcode
  2. 业务组件整合平台
  3. Lowcode 使用情况监控

第一个方向挺简单的,Boss 希望 Lowcode 能够同时具备移动端和 pc 端的搭建能力,在配合已经抽象好的 DataSet 基础之上,实现的话也就是更换渲染引擎与设计器而已,挑战点会集中在渲染引擎部分(如何将 json 数据翻译成移动端能识别的数据)

第二个方向是我力主要完成的点,这个平台主要是为了解决一个核心问题:项目交付人员如何复用之前交付时的交付产物。copy paste 作为一种方案当然可行,问题在于低效,因此我希望能够有一种平台,可以将所有这些交付产物管理起来,并在下一次交付的时候能够快速的开箱即用。

第三个方向是为了补足我们目前存在的问题,项目很多的特性都是参照其他的产品实现去做的,但是这些特性是否真的适合我们自身的产品?衡量指标是什么?客户是否真的对这些特性买账?这些都是我们未来需要回答的问题。

小结

记得上个月看到 Thoughtworks 的技术负责人痛批 Lowcode,在我看来实属用力过度了。这类产品的意义从来不在于通用,而是解决某中特定场景下的交付痛点。观察目前我们试点项目的反馈情况,简单场景下的交付还是具备不错的反响的。因此这一类产品,一定是结合公司的个体情况去做对应的设计,才能取得好的反响。

同样的,从这个角度去看问题,可以发现 Lowcode 像是一套公司资产的收集器,将过去公司交付所产生的资产做一个统一的规约,从而利用公司过去所忽视的这些交付产物形成一个生态,在自己的业务领域打出独有的优势。这就是 Lowcode 这类产品到目前为止依然层出不穷的原因所在。

最后总结一下,Lowcode 的提效从来不在于拖拽,也不在于可视化,一切提效的手段本质上是简化操作,以及对之前成果的复用,仅此而已。

杂谈

其实上我们整个团队,在制作 Lowcode 的时候一直都在思考一个问题:

如果我司的低代码平台成熟了,希望用业务代替所有研发的愿景也实现了,那我们的交付人员应当何去何从呢?

如果一个业务能用 Lowcode 平台稳定交付项目,这时他能否被称作是程序员?

这些问题都很具备思考价值,大公司也许不需要去考虑这种问题,因为人员素质足够高,被解放出来的人力资源可以投入更有价值的研究工作中。但是像我们这样的小公司,交付人员在研发体系成熟后应当何去何从?是否会像纺织机发明后失业的纺织工一样呢?

这些问题我也暂且没有思考出明确的答案,也许未来的某一天会得到真正的答案吧,届时我会再更新一篇后续文章的。

© 2020 — Douglas/rss
友情连接/卡拉搜索